/**
 * This file is Copyright (C)2009 by <a href="http://www.blackbit.net">Blackbit Consulting bvba</a>, Belgium.
 * This source code is released under the GPL License, and may be freely distributed. 
 */
package net.blackbit.util.json;

import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Map;

/**
 * Writer for creating writing objects to a JSON compliant output.
 * 
 * @author DKETS
 */
public class JSONWriter extends PrintWriter{
	
	/**
	 * Constructs a new JSON writer that will write to the specified writer.
	 * @param target The target writer.
	 */
	public JSONWriter(Writer target) {
		super(target);
	}
	
	/**
	 * Constructs a new JSON writer that will write to the specified output stream.
	 * @param outputStream The output stream.
	 */
	public JSONWriter(OutputStream outputStream) {
		super(outputStream);
	}
	
	/**
	 * Writer a value to the JSON output.
	 * The output will be closed after ONE cycle!
	 * @param object The object to write.
	 */
	public void writeJSON(Object value){
		writeJSONValue(value);
		close();
	}
	
	/**
	 * Inner method for writing a value to the output.
	 * 
	 * @param object The value to write.
	 */
	private void writeJSONValue(Object object){
		if (object == null){ // Null
			writeJSONNull();
		} else if (object instanceof String){ // Strings
			writeJSONString((String)object);
		} else if (Number.class.isInstance(object)){ // Numbers
			writeJSONNumber((Number)object);
		} else if (Boolean.class.isInstance(object)){ // Booleans
			writeJSONBoolean((Boolean)object);
		} else if (object.getClass().isArray()){ // Array
			writeJSONArray(object);
		} else if (object instanceof Map){ // Map
			writeJSONMap((Map<?,?>)object);
		} else if (object instanceof Collection) { // Collection
			writeJSONCollection((Collection<?>)object);
		} else { // Object
			writeJSONObject(object);
		}		
	}
	
	/**
	 * Inner method for writing an object.
	 * @param object The object.
	 */
	private void writeJSONObject(Object object){
		append('{'); // Open
		Field[] fields = object.getClass().getFields();
		int finsih = fields.length -1;
		for (int i = 0; i < fields.length; i ++){
			Field field = fields[i];
			if (!field.isAccessible()){
				try {
					field.setAccessible(true);
				} catch (SecurityException securityException){}
			}
			if (field.isAccessible()){
				try {
					writeJSONPair(field.getName(), field.get(object));
					if (i!=finsih){
						append(", ");
					}
				} catch (IllegalAccessException illegalAccessException){
					illegalAccessException.printStackTrace();
				}
			}
		}
		append('}'); // Close
	}
	
	/**
	 * Inner method for writing a map.
	 * @param map The map.
	 */
	private void writeJSONMap(Map<?,?> map){
		append('{'); // Open		 
		int finish = map.size()-1;
		int i = 0;
		for (Map.Entry<?,?> entry : map.entrySet()){			
			writeJSONMapEntry(entry);
			if (i!=finish){
				append(", ");
			}
			i++;
		}
		append('}'); // Close
	}

	/**
	 * Inner method for writing an entry.
	 * @param entry The map entry.
	 */
	private void writeJSONMapEntry(Map.Entry<?, ?> entry) {
		writeJSONPair(String.valueOf(entry.getKey()), entry.getValue());
	}
	
	/**
	 * Inner method for writing a collection.
	 * @param collection The collection.
	 */
	private void writeJSONCollection(Collection<?> collection){
		append('['); // Open		 
		int finish = collection.size()-1;
		int i = 0;
		for (Object value : collection){			
			writeJSONValue(value);
			if (i!=finish){
				append(", ");
			}
			i++;
		}
		append(']'); // Close
	}
	
	/**
	 * Inner method for writing an array.
	 * @param array The array.
	 */
	private void writeJSONArray(Object array){
		append('[');
		int length = Array.getLength(array);
		int finish = length-1;
		for (int i = 0; i < length; i ++){
			writeJSONValue(Array.get(array, i));
			if (i!=finish){
				append(", ");
			}
		}		
		append(']');
	}
	
	/**
	 * Inner method for writing null.
	 */
	private void writeJSONNull(){
		append("null");
	}
	
	/**
	 * Inner method for writing a pair.
	 * @param name The pair name.
	 * @param value The pair value.
	 */
	private void writeJSONPair(String name, Object value){
		writeJSONString(name);
		append(": ");
		writeJSONValue(value);
	}
	
	/**
	 * Inner method for writing a number.	
	 * @param number The number.
	 */
	private void writeJSONNumber(Number number){
		append(number.toString());
	}
	
	/**
	 * Inner method for writing a boolean.
	 * @param value The boolean value.
	 */
	private void writeJSONBoolean(Boolean value){
		append(value.toString());
	}
	
	/**
	 * Inner method for writing a string.
	 * @param value The string.
	 */
	private void writeJSONString(String value){
		append('\"');
		escape(value);
		append('\"');
	}
	
	/**
	 * Inner method for writing an escaped string.
	 * @param string The string to escape.
	 */
	private void escape(String string) {
		for(int i=0;i<string.length();i++){
			char ch=string.charAt(i);
			switch(ch){
			case '"':
				append("\\\"");
				break;
			case '\\':
				append("\\\\");
				break;
			case '\b':
				append("\\b");
				break;
			case '\f':
				append("\\f");
				break;
			case '\n':
				append("\\n");
				break;
			case '\r':
				append("\\r");
				break;
			case '\t':
				append("\\t");
				break;
			case '/':
				append("\\/");
				break;
			default:
				if((ch>='\u0000' && ch<='\u001F') || (ch>='\u007F' && ch<='\u009F') || (ch>='\u2000' && ch<='\u20FF')){
					String ss=Integer.toHexString(ch);
					append("\\u");
					for(int k=0;k<4-ss.length();k++){
						append('0');
					}
					append(ss.toUpperCase());
				}
				else{
					append(ch);
				}
			}
		}
	}
}
